/*
 * Copyright (c) 2012 Johan Fylling
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy 
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights 
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
 * copies of the Software, and to permit persons to whom the Software is 
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in 
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
 * THE SOFTWARE.
 */

part of engine;


abstract class DebugBrush {
  bool   worldCoordinates;
  String style;
  
  DebugBrush (String this.style, bool this.worldCoordinates);
  
  void draw (var context, Camera camera);
}

class DebugLine extends DebugBrush {
  final Vector2 from;
  final Vector2 to;
  
  DebugLine (Vector2 from, 
             Vector2 to, 
             String style, 
             bool worldCoordinates) : super(style, worldCoordinates),
                                      this.from = new Vector2.copy(from), 
                                      this.to = new Vector2.copy(to);
  
  void draw (var context, Camera camera) {
    context.strokeStyle = style;
    context.beginPath();
    context.moveTo(from.x, from.y);
    context.lineTo(to.x, to.y);
    context.stroke();
  }
}

class DebugPoint extends DebugBrush {
  Vector2 position;
  
  DebugPoint (Vector2 position, 
              String style,
              bool worldCoordinates) : super(style, worldCoordinates),
                                       this.position = new Vector2.copy(position);
  
  void draw (var context, Camera camera) {
    context.strokeStyle = style;
    context.beginPath();
    context.arc(position.x, 
                position.y,
                3*camera.scaleInv,
                0,
                Math.PI*2,
                false);
    context.fill();
    context.closePath();
    context.stroke();
  }
}

class DebugCoordinates extends DebugBrush {
  final Vector2 coordinates;
  
  DebugCoordinates (Vector2 coordinates, 
                    String style, 
                    bool worldCoordinates) : super(style, worldCoordinates),
                                             this.coordinates = new Vector2.copy(coordinates);
  
  void draw (var context, Camera camera) {
    context.strokeStyle = style;
    if (worldCoordinates) {
      context.beginPath();
      context.moveTo(camera.position.x - camera.rectangle.halfSize.x, 
                     this.coordinates.y);
      context.lineTo(camera.position.x + camera.rectangle.halfSize.x, 
                     this.coordinates.y);
      context.stroke();
      
      context.beginPath();
      context.moveTo(this.coordinates.x, 
                     camera.position.y - camera.rectangle.halfSize.y);
      context.lineTo(this.coordinates.x, 
                     camera.position.y + camera.rectangle.halfSize.y);
      context.stroke();
    } else {
      context.beginPath();
      context.moveTo(0, 
                     this.coordinates.y);
      context.lineTo(camera.widthCanvas, 
                     this.coordinates.y);
      context.stroke();
      
      context.beginPath();
      context.moveTo(this.coordinates.x, 
                     0);
      context.lineTo(this.coordinates.x, 
                     camera.heightCanvas);
      context.stroke();
    }
  }
}

class CanvasEngine implements GraphicsEngine, DebugPainter {
  CanvasElement           _canvas;
  var                     _context;
  num                     _width;
  num                     _height;
  
  bool                    drawParticles                 = false;
  bool                    drawCollisionInfo             = false;
  bool                    drawDebug                     = false;
  
  final BroadphaseDetector _bpd;
  
  final Camera            camera                        = new Camera();
  
  String                  aabbFillStyle                 = "rgba(0,0,0,0.0)";
  String                  aabbStrokeStyle               = "rgba(128,128,128,0.5)";
  String                  aabbCollidingFillStyle        = "rgba(0,0,0,0.0)";
  String                  aabbCollidingStrokeStyle      = "rgba(255,0,0,1.0)";
  String                  collidableFillStyle           = "rgba(0,0,255,0.125)";
  String                  collidableStrokeStyle         = "rgba(0,0,255,0.5)";
  String                  collidableInactiveFillStyle   = "rgba(64,64,64,0.125)";
  String                  collidableInactiveStrokeStyle = "rgba(128,128,128,1.0)";
  String                  navFillStyle                  = "rgba(255,255,255,0.125)";
  String                  navStrokeStyle                = "rgba(128,128,128,0.75)";
  
  final List<DebugBrush> _debugBrushesWorld             = new List<DebugBrush>();
  final List<DebugBrush> _debugBrushesCanvas            = new List<DebugBrush>();
  
  BroadphaseDetector bpd = null;
  
  CanvasEngine(String canvasId, BroadphaseDetector this._bpd){
    this._canvas = query("#$canvasId");
    this._context = this._canvas.context2d;
    
    window.setImmediate(() {
      _width = (this._canvas.parent as Element).clientWidth;
      _height = (this._canvas.parent as Element).clientHeight;
      this._canvas.width = _width;
      this._canvas.height = _height;
      (this.camera.collidable as CollidableRectangle).halfSize = new Vector2(this._width/2, this._height/2);
    });
  }
  
  num get width => this._width;
  
  num get height => this._height;
  
  void step (WorldState ws) {
    this._context.lineWidth = 0.5;
    this._context.save();
    
    this._context.fillStyle = "white";
    this._context.rect(0, 0, this.width, this.height);
    this._context.fill();
    
    // Camera.
    (this.camera.collidable as CollidableRectangle).halfSize = new Vector2((this._width*this.camera.scaleInv)/2, (this._height*this.camera.scaleInv)/2);
    this._context.scale(this.camera.scale, this.camera.scale);
    this._context.translate(-this.camera.position.x + (this.camera.collidable as CollidableRectangle).halfSize.x, 
                            -this.camera.position.y + (this.camera.collidable as CollidableRectangle).halfSize.y);
    //this._context.rotate(-this.camera.angle);
    this._context.lineWidth = 0.5 * this.camera.scaleInv;
    
    // Draw Effects.
    _drawEffects(ws);
    
    // Draw World Objects.
    _drawObjects(ws);
    
    // Draw Particles.
    if (drawParticles && ws is ParticleWorldState) {
      _drawParticles(ws as ParticleWorldState);
    }
    
    // This is pretty ugly, and will be fixed as soon as possible.
    if (drawCollisionInfo && this.bpd != null) {
      this._context.fillStyle = this.aabbFillStyle;
      this._context.strokeStyle = this.aabbStrokeStyle;
      
      AABB aabb1 = new AABB();
      AABB aabb2 = new AABB();
      
      for (CollisionPair pair in this.bpd.collisionPairs) {
        if (pair.colliding) {
          this._context.fillStyle = this.aabbCollidingFillStyle;
          this._context.strokeStyle = this.aabbCollidingStrokeStyle;
        } else {
          this._context.fillStyle = this.aabbFillStyle;
          this._context.strokeStyle = this.aabbStrokeStyle;
        }
        pair.body1.collidable.getAABB(aabb1, pair.body1);
        pair.body2.collidable.getAABB(aabb2, pair.body2);
        num minX = Math.min(aabb1.min.x, aabb2.min.x);
        num maxX = Math.max(aabb1.max.x, aabb2.max.x);
        num minY = Math.min(aabb1.min.y, aabb2.min.y);
        num maxY = Math.max(aabb1.max.y, aabb2.max.y);
        this._context.beginPath();
        this._context.rect(minX, 
                           minY, 
                           maxX - minX, 
                           maxY - minY);
        this._context.stroke();
      }
    }
    
    _drawDebugWorld();
    
    this._context.restore();
    
    _drawDebugCanvas();
  }
  
  void _drawObjects (WorldState ws) {
    // Draw Collidables.
    this._context.fillStyle = this.collidableFillStyle;
    this._context.strokeStyle = this.collidableStrokeStyle;
    
    AABB cameraAabb = new AABB();
    this.camera.collidable.getAABB(cameraAabb, this.camera);
    List<DynamicBody> bodies = this.bpd.cull(cameraAabb);
    
    AABB aabb = new AABB();
    
    for(DynamicBody body in bodies) {
      if (body.collidable != null) {
        if (body.active) {
          this._context.fillStyle = this.collidableFillStyle;
          this._context.strokeStyle = this.collidableStrokeStyle;
        } else {
          this._context.fillStyle = this.collidableInactiveFillStyle;
          this._context.strokeStyle = this.collidableInactiveStrokeStyle;
        }
        if (body.collidable is CollidableCircle) {
          CollidableCircle cc = body.collidable as CollidableCircle;
          this._context.beginPath();
          this._context.arc(body.position.x, 
                            body.position.y,
                            cc.radius,
                            0,
                            Math.PI*2,
                            false);
          this._context.fill();
          this._context.stroke();
          this._context.closePath();
        } else if (body.collidable is CollidableRectangle) {
          CollidableRectangle cr = body.collidable as CollidableRectangle;
          Vector2 halfSize = cr.halfSize;
          this._context.save();
          this._context.beginPath();
          this._context.translate(body.position.x, 
                                  body.position.y);
          this._context.rotate(body.angle);
          this._context.rect(-halfSize.x, 
                             -halfSize.y, 
                             halfSize.x*2, 
                             halfSize.y*2);
          this._context.fill();
          this._context.stroke();
          this._context.restore();
        }
      }
    }
    
    if (drawCollisionInfo) {
      this._context.fillStyle = this.aabbFillStyle;
      this._context.strokeStyle = this.aabbStrokeStyle;
      for(WorldObject wo in ws.objects) {
        if (wo.body != null && wo.body.collidable != null) {
          Vector2 center = wo.body.position + wo.body.collidable.boundingSphereOffset;
          
          this._context.beginPath();
          this._context.arc(center.x, 
                            center.y,
                            wo.body.collidable.boundingSphereRadius,
                            0,
                            Math.PI*2,
                            false);
          this._context.closePath();
          this._context.stroke();
          
          wo.body.collidable.getAABB(aabb, wo.body);
          this._context.beginPath();
          this._context.rect(aabb.min.x, 
                             aabb.min.y, 
                             aabb.width, 
                             aabb.height);
          this._context.stroke();
        }
      }
    }
    
    // Draw orientation, heading and field of view.
    num pointerLength = 10 * this.camera.scaleInv;
    this._context.strokeStyle = this.navStrokeStyle;
    for(DynamicBody body in bodies) {
      this._context.beginPath();
      this._context.arc(body.position.x, 
                        body.position.y,
                        pointerLength,
                        0,
                        Math.PI*2,
                        false);
      this._context.closePath();
      this._context.stroke();
      
      // Draw object heading.
      Vector2 heading = body.velocity.normalized();
      this._context.beginPath();
      this._context.moveTo(body.position.x, 
                           body.position.y);
      this._context.lineTo(body.position.x + heading.x*pointerLength, 
                           body.position.y + heading.y*pointerLength);
      this._context.stroke();
      
      // Draw object orientation.
      Vector2 orientation = new Vector2.fromAngle(body.angle);
      this._context.beginPath();
      this._context.moveTo(body.position.x + orientation.x*pointerLength, 
                           body.position.y + orientation.y*pointerLength);
      this._context.lineTo(body.position.x + orientation.x*pointerLength*2, 
                           body.position.y + orientation.y*pointerLength*2);
      this._context.stroke();
      
      // Draw field of view.
      if (body.data is Actor) {
        CollidableCircle fovCircle = body.data.fieldOfView.body.collidable;
        this._context.beginPath();
        this._context.arc(body.position.x,
                          body.position.y,
                          fovCircle.radius,
                          0,
                          Math.PI*2,
                          false);
        this._context.closePath();
        this._context.stroke();
      }
    }
  }
  
  void _drawParticles (ParticleWorldState pws) {
    this._context.fillStyle = "none";
    this._context.strokeStyle = "gray";
    double particleSize = 3*this.camera.scaleInv;
    
    for (ParticleSystem ps in pws.particleSystems) {
      for(Particle p in ps.particles) {
        this._context.beginPath();
        this._context.arc((p.position as Vector2).x, 
                          (p.position as Vector2).y,
                          particleSize,
                          0,
                          Math.PI*2,
                          false);
        this._context.fill();
        this._context.closePath();
        this._context.stroke();
      }
      
      for (Constraint c in ps.constraints) {
        if (c is DistanceConstraint) {
          var dc = c as DistanceConstraint;
          Vector2 from = dc.endA;
          Vector2 to = dc.endB;
          this._context.beginPath();
          this._context.moveTo(from.x, from.y);
          this._context.lineTo(to.x, to.y);
          this._context.stroke();
        }
      }
    }
  }
  
  void _drawEffects (WorldState ws) {
    for(Effect effect in ws.effects) {
      if (effect.body != null && effect.body.collidable != null) {
        if (effect.body.active) {
          this._context.fillStyle = this.collidableFillStyle;
          this._context.strokeStyle = this.collidableStrokeStyle;
        } else {
          this._context.fillStyle = this.collidableInactiveFillStyle;
          this._context.strokeStyle = this.collidableInactiveStrokeStyle;
        }
        if (effect.body.collidable is CollidableCircle) {
          CollidableCircle cc = effect.body.collidable as CollidableCircle;
          this._context.beginPath();
          this._context.arc(effect.body.position.x, 
                            effect.body.position.y,
                            cc.radius,
                            0,
                            Math.PI*2,
                            false);
          this._context.fill();
          this._context.stroke();
          this._context.closePath();
        } else if (effect.body.collidable is CollidableRectangle) {
          CollidableRectangle cr = effect.body.collidable as CollidableRectangle;
          Vector2 halfSize = cr.halfSize;
          this._context.save();
          this._context.beginPath();
          this._context.translate(effect.body.position.x, 
                                  effect.body.position.y);
          this._context.rotate(effect.body.angle);
          this._context.rect(-halfSize.x, 
                             -halfSize.y, 
                             halfSize.x*2, 
                             halfSize.y*2);
          this._context.fill();
          this._context.stroke();
          this._context.restore();
        } else if (effect.body.collidable is CollidableRay) {
          CollidableRay cr = effect.body.collidable as CollidableRay;
          this._context.beginPath();
          this._context.moveTo(effect.body.position.x, 
                               effect.body.position.y);
          this._context.lineTo(effect.body.position.x + cr.target.x, 
                               effect.body.position.y + cr.target.y);
          this._context.stroke();
        }
      }
    }
  }
  
  void debugDrawLine (Vector from, Vector to, {String style:"red", bool world:true}) {
    if (!drawDebug || from is! Vector2 || to is! Vector2) {
      return;
    }
    
    if (world) {
      this._debugBrushesWorld.add(new DebugLine(from, to, style, true));
    } else {
      this._debugBrushesCanvas.add(new DebugLine(from, to, style, false));
    }
  }
  
  void debugDrawPoint (Vector position, {String style:"red", bool world:true}) {
    if (!drawDebug || position is! Vector2) {
      return;
    }
    
    if (world) {
      this._debugBrushesWorld.add(new DebugPoint(position, style, true));
    } else {
      this._debugBrushesCanvas.add(new DebugPoint(position, style, false));
    }
  }
  
  void debugDrawCoordinates (Vector coordinate, {String style:"red", bool world:true}) {
    if (!drawDebug || coordinate is! Vector2) {
      return;
    }
    
    if (world) {
      this._debugBrushesWorld.add(new DebugCoordinates(coordinate, style, true));
    } else {
      this._debugBrushesCanvas.add(new DebugCoordinates(coordinate, style, false));
    }
  }
  
  void _drawDebugWorld () {
    for (DebugBrush brush in this._debugBrushesWorld) {
      brush.draw(this._context, this.camera);
    }
    this._debugBrushesWorld.clear();
  }
  
  void _drawDebugCanvas () {
    for (DebugBrush brush in this._debugBrushesCanvas) {
      brush.draw(this._context, this.camera);
    }
    this._debugBrushesCanvas.clear();
  }
  
  void toggleDrawDebug() {
    this.drawDebug = !this.drawDebug;
  }
  
  void toggleDrawDebugCollisions() {
    this.drawCollisionInfo = !this.drawCollisionInfo;
  }
  
  void toggleDrawDebugParticles() {
    this.drawParticles = !this.drawParticles;
  }
}
